UNPKG

@frames.js/debugger

Version:

Debug your Frames.js applications

355 lines (321 loc) 9.57 kB
import { type SendNotificationRequest } from "@farcaster/frame-sdk"; import type { NextRequest } from "next/server"; import { z } from "zod"; import { FrameServerEvent, InvalidWebhookResponseError, sendEvent, } from "frames.js/farcaster-v2/events"; import { type Hex, hexToBytes } from "viem"; import { getStorage } from "../storage"; import { notificationDetailsSchema } from "../parsers"; export type NotificationUrl = { token: string; }; export type Notification = Omit<SendNotificationRequest, "tokens">; const privateKeyParser = z .string() .min(1) .startsWith("0x") .transform((val) => hexToBytes(val as Hex)); const postRequestBodySchema = z.discriminatedUnion("action", [ z.object({ action: z.literal("add_frame"), privateKey: privateKeyParser, }), z.object({ action: z.literal("remove_frame"), privateKey: privateKeyParser, }), z.object({ action: z.literal("enable_notifications"), privateKey: privateKeyParser, }), z.object({ action: z.literal("disable_notifications"), privateKey: privateKeyParser, }), ]); export type POSTNotificationsDetailRequestBody = z.input< typeof postRequestBodySchema >; const postResponseBodySchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("frame_added"), notificationDetails: notificationDetailsSchema, }), z.object({ type: z.literal("frame_removed"), }), z.object({ type: z.literal("notifications_enabled"), notificationDetails: notificationDetailsSchema, }), z.object({ type: z.literal("notifications_disabled"), }), ]); export type POSTNotificationsDetailResponseBody = z.infer< typeof postResponseBodySchema >; /** * Handles different operations on namespace level */ export async function POST( req: NextRequest, { params }: { params: { namespaceId: string } } ) { const requestBody = postRequestBodySchema.safeParse(await req.json()); if (!requestBody.success) { return Response.json(requestBody.error.flatten(), { status: 400 }); } const storage = await getStorage(req.nextUrl.href); const namespace = await storage.getNamespace(params.namespaceId); if (!namespace) { return Response.json({ message: "Not found" }, { status: 404 }); } switch (requestBody.data.action) { case "add_frame": { const notificationDetails = await storage.addFrame(namespace); const eventId = crypto.randomUUID(); await storage.recordEvent(namespace.id, { type: "event", event: { event: "frame_added", notificationDetails, }, id: eventId, }); const event: FrameServerEvent = { event: "frame_added", notificationDetails, }; sendEvent(event, { privateKey: requestBody.data.privateKey, fid: namespace.fid, webhookUrl: namespace.webhookUrl, }) .then(() => { return storage.recordEvent(namespace.id, { type: "event_success", event, eventId, id: crypto.randomUUID(), }); }) .catch(async (e) => { return storage.recordEvent(namespace.id, { type: "event_failure", event, eventId, id: crypto.randomUUID(), message: e instanceof Error ? e.message : String(e), response: e instanceof InvalidWebhookResponseError ? { body: await e.response.text(), headers: Object.fromEntries(e.response.headers.entries()), status: e.response.status, } : undefined, }); }); return Response.json( postResponseBodySchema.parse({ type: "frame_added", notificationDetails, }), { status: 201 } ); } case "remove_frame": { await storage.removeFrame(namespace); const eventId = crypto.randomUUID(); const event: FrameServerEvent = { event: "frame_removed", }; await storage.recordEvent(namespace.id, { type: "event", event, id: eventId, }); sendEvent(event, { fid: namespace.fid, privateKey: requestBody.data.privateKey, webhookUrl: namespace.webhookUrl, }) .then(() => { return storage.recordEvent(namespace.id, { type: "event_success", event, eventId, id: crypto.randomUUID(), }); }) .catch(async (e) => { return storage.recordEvent(namespace.id, { type: "event_failure", event, eventId, id: crypto.randomUUID(), message: e instanceof Error ? e.message : String(e), response: e instanceof InvalidWebhookResponseError ? { body: await e.response.text(), headers: Object.fromEntries(e.response.headers.entries()), status: e.response.status, } : undefined, }); }); return Response.json( postResponseBodySchema.parse({ type: "frame_removed" }), { status: 200, } ); } case "enable_notifications": { const notificationDetails = await storage.enableNotifications(namespace); const eventId = crypto.randomUUID(); await storage.recordEvent(namespace.id, { type: "event", event: { event: "notifications_enabled", notificationDetails, }, id: eventId, }); const event: FrameServerEvent = { event: "notifications_enabled", notificationDetails, }; sendEvent(event, { fid: namespace.fid, privateKey: requestBody.data.privateKey, webhookUrl: namespace.webhookUrl, }) .then(() => { return storage.recordEvent(namespace.id, { type: "event_success", event, eventId, id: crypto.randomUUID(), }); }) .catch(async (e) => { return storage.recordEvent(namespace.id, { type: "event_failure", event, eventId, id: crypto.randomUUID(), message: e instanceof Error ? e.message : String(e), response: e instanceof InvalidWebhookResponseError ? { body: await e.response.text(), headers: Object.fromEntries(e.response.headers.entries()), status: e.response.status, } : undefined, }); }); return Response.json( postResponseBodySchema.parse({ type: "notifications_enabled", notificationDetails, }), { status: 201 } ); } case "disable_notifications": { await storage.disableNotifications(namespace); const eventId = crypto.randomUUID(); await storage.recordEvent(namespace.id, { type: "event", event: { event: "notifications_disabled", }, id: eventId, }); const event: FrameServerEvent = { event: "notifications_disabled", }; sendEvent(event, { fid: namespace.fid, privateKey: requestBody.data.privateKey, webhookUrl: namespace.webhookUrl, }) .then(() => { return storage.recordEvent(namespace.id, { type: "event_success", event, eventId, id: crypto.randomUUID(), }); }) .catch(async (e) => { return storage.recordEvent(namespace.id, { type: "event_failure", event, eventId, id: crypto.randomUUID(), message: e instanceof Error ? e.message : String(e), response: e instanceof InvalidWebhookResponseError ? { body: await e.response.text(), headers: Object.fromEntries(e.response.headers.entries()), status: e.response.status, } : undefined, }); }); return Response.json( postResponseBodySchema.parse({ type: "notifications_disabled" }), { status: 200, } ); } default: { requestBody.data satisfies never; throw new Error(`Unknown action`); } } } export const getResponseBodySchema = z.object({ fid: z.number(), frameAppUrl: z.string().url(), namespaceUrl: z.string().url(), webhookUrl: z.string().url(), frame: z.discriminatedUnion("status", [ z.object({ status: z.literal("added"), notificationDetails: notificationDetailsSchema.nullable(), }), z.object({ status: z.literal("removed"), }), ]), }); export type GETNotificationsDetailResponseBody = z.infer< typeof getResponseBodySchema >; /** * Gets the namespace settings */ export async function GET( req: NextRequest, { params }: { params: { namespaceId: string } } ): Promise<Response> { const storage = await getStorage(req.nextUrl.href); const namespace = await storage.getNamespace(params.namespaceId); if (!namespace) { return Response.json({ message: "Not found" }, { status: 404 }); } return Response.json(getResponseBodySchema.parse(namespace), { status: 200, }); }